
//npm install  thirt-two
const crypto = require('crypto');
const ThirtyTwo = require('thirty-two');

export enum Digest {
    SHA1,
    SHA256,
    SHA512
}


export class TOTP {
    protected key: Buffer;
    protected digits: number;
    protected digest: Digest;
    protected interval: number;
    constructor(s:string, digits: number = 6, digest: Digest = Digest.SHA1, interval: number = 30) {
        this.key = ThirtyTwo.decode(s);
        this.digits = digits;
        this.digest = digest;
        this.interval = interval; 
    }
    
    timecode(forTime: Date) {
        const currentTime = Math.floor(forTime.getTime() / 1000);
        return Math.floor(currentTime / this.interval);
    }

    hmac(data: Uint8Array) {
        let algorithm : string | undefined;
        switch (this.digest) {
            case Digest.SHA1:
                algorithm = 'sha1';
                break;
            case Digest.SHA256:
                algorithm = 'sha256';
                break;
            case Digest.SHA512:
                algorithm = 'sha512';
                break;
        }

        const hmac = crypto.createHmac(algorithm, this.key);
        hmac.update(data);
        return hmac.digest();
    }

    dynamicTruncation(hmacResult: Buffer) {
        const offset = hmacResult[hmacResult.length - 1] & 0x0f;
        return (
            ((hmacResult[offset] & 0x7f) << 24) |
            ((hmacResult[offset + 1] & 0xff) << 16) |
            ((hmacResult[offset + 2] & 0xff) << 8) |
            (hmacResult[offset + 3] & 0xff)
        );
    }

    at(forTime: Date) {
        const timeCounter = this.timecode(forTime);
        const timeBytes = longToByteArray(timeCounter);
        const hmacResult = this.hmac(timeBytes);
        const binaryCode = this.dynamicTruncation(hmacResult);
        const otp = binaryCode % Math.pow(10, this.digits);
        return String(otp).padStart(this.digits, '0');

    }

    now() {
        return this.at(new Date());
    }
}

function longToByteArray(long) {
    const byteArray = new Uint8Array(8);
    for (let i = 7; i >= 0; i--) {
        byteArray[i] = long & 0xff;
        long = long >> 8;
    }
    return byteArray;
}

export interface TotpInfo {
    issuer?: string;
    accountName: string;
    secret: string;
    digits?: number;
    period?: number;
    algorithm: string | null;
}

export function parseTotpUri(uri: string): TotpInfo {
    if (!uri.startsWith('otpauth://totp/')) {
        throw new Error('Invalid TOTP URI. It should start with otpauth://totp/.');
    }

    const pathAndQuery = uri.slice('otpauth://totp/'.length);
    const [path, queryString] = pathAndQuery.split('?');
    let issuer: string | undefined;
    let accountName: string;
    if (path.includes(':')) {
        [issuer, accountName] = path.split(':');
    } else {
        accountName = path;
    }

    const queryParams = new URLSearchParams(queryString);
    const secret = queryParams.get('secret');
    if (!secret) {
        throw new Error('Missing "secret" parameter in TOTP URI.');
    }

    const digits = queryParams.has('digits')? parseInt(queryParams.get('digits') as string, 10) : undefined;
    const period = queryParams.has('period')? parseInt(queryParams.get('period') as string, 10) : undefined;
    const algorithm = queryParams.get('algorithm');

    // 如果查询参数中有 issuer，优先使用查询参数中的 issuer
    if (queryParams.has('issuer')) {
        issuer = queryParams.get('issuer') as string;
    }

    return {
        issuer,
        accountName,
        secret,
        digits,
        period,
        algorithm
    };
}
